/*
 * Copyright (c) KylinSoft  Co., Ltd. 2024.All rights reserved.
 * ha-api licensed under the Mulan Permissive Software License, Version 2.
 * See LICENSE file for more details.
 * Author: bixiaoyan <bixiaoyan@kylinos.cn>
 * Date: Thu Mar 27 09:32:28 2025 +0800
 */

package models

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net"
	"os"
	"regexp"
	"strconv"
	"strings"

	"gitee.com/openeuler/ha-api/settings"
	"gitee.com/openeuler/ha-api/utils"
	"github.com/chai2010/gettext-go"

	"errors"
)

type HostInfo struct {
	IP     string `json:"ip"`
	NodeID string `json:"nodeid"`
}

func GetHeartBeatHosts() ([]HostInfo, error) {
	knownHosts := []HostInfo{}

	out, err := utils.RunCommand(utils.CmdGetPcsdAuthFile)
	if err != nil {
		return nil, errors.New("no node in the Cluster, please run \"pcs host auth $nodename\" to add node")
	}

	jsonData := map[string]interface{}{}

	if err := json.Unmarshal(out, &jsonData); err != nil {
		return nil, errors.New("parse host info failed")
	}
	for k := range jsonData["known_hosts"].(map[string]interface{}) {
		hostInfo := HostInfo{
			IP:     "",
			NodeID: k,
		}
		knownHosts = append(knownHosts, hostInfo)
	}

	return knownHosts, nil
}

func GetHeartBeatDictionary() (interface{}, error) {
	nodeList, err := utils.GetNodeList()
	if err != nil {
		return nil, err
	}

	res := map[string][]map[string]string{}
	for _, node := range nodeList {
		name := node["name"]
		for k, addr := range node {
			if k != "name" && k != "nodeid" {
				info := map[string]string{}
				info["nodeid"] = name
				info["ip"] = addr

				if _, ok := res[k]; !ok {
					res[k] = []map[string]string{}
				}
				res[k] = append(res[k], info)
			}
		}
	}

	ret := map[string][]map[string]string{}
	count := 0
	for _, value := range res {
		count++
		hbStr := "hbaddrs" + strconv.Itoa(count)
		ret[hbStr] = value
	}

	return ret, nil
}

func GetHeartBeatConfig() (interface{}, error) {
	var rst interface{}
	var err error
	if utils.IsClusterExist() {
		rst, err = GetHeartBeatDictionary()
	} else {
		rst, err = GetHeartBeatHosts()
	}

	if err != nil {
		return nil, err
	}
	return rst, nil
}

func EditHeartbeatInfo(jsonData []byte) error {
	if len(jsonData) == 0 {
		return errors.New(gettext.Gettext("No input data"))
	}

	data := struct {
		Hbaddrs1        []HostInfo `json:"hbaddrs1"`
		Hbaddrs2        []HostInfo `json:"hbaddrs2"`
		Hbaddrs2Enabled int        `json:"hbaddrs2_enabled"`
	}{}
	if err := json.Unmarshal(jsonData, &data); err != nil {
		return errors.New(gettext.Gettext("invalid config data"))
	}

	heartBeatInfos := map[string]([]string){}
	if len(data.Hbaddrs1) > 0 {
		for _, info := range data.Hbaddrs1 {
			if _, ok := heartBeatInfos[info.NodeID]; !ok {
				heartBeatInfos[info.NodeID] = []string{}
			}
			heartBeatInfos[info.NodeID] = append(heartBeatInfos[info.NodeID], info.IP)
		}
	}
	if len(data.Hbaddrs2) > 0 {
		for _, info := range data.Hbaddrs2 {
			if _, ok := heartBeatInfos[info.NodeID]; !ok {
				heartBeatInfos[info.NodeID] = []string{}
			}
			heartBeatInfos[info.NodeID] = append(heartBeatInfos[info.NodeID], info.IP)
		}
	}

	cmd := utils.CmdSetupCluster
	for key, value := range heartBeatInfos {
		cmd = cmd + " " + key
		for _, v := range value {
			addr := "addr=" + v
			cmd = cmd + " " + addr
		}
	}
	cmd = cmd + " --force --start"

	runResource := false
	// TODO: check logic
	if _, err := utils.RunCommand(utils.CmdClusterStatusAsXML); err != nil {
		// means a cluster is already running
		runResource = false
	} else {
		runResource = true
	}

	if runResource {
		if _, err := utils.RunCommand(utils.CmdSaveCIB); err != nil {
			goto ret
		}
		if _, err := utils.RunCommand(utils.CmdResourceCleanup); err != nil {
			goto ret
		}
		if _, err := utils.RunCommand(utils.CmdStopCluster); err != nil {
			goto ret
		}
		if _, err := utils.RunCommand(utils.CmdDestroyClusterForce); err != nil {
			goto ret
		}
		if _, err := utils.RunCommand(cmd); err != nil {
			goto ret
		}
		if _, err := utils.RunCommand(utils.CmdPushFileToCIB); err != nil {
			goto ret
		}

		return nil
	}

	return errors.New(gettext.Gettext("there are running resources in the cluster, please close first"))

ret:
	return errors.New(gettext.Gettext("Change cluster failed"))
}

func DeleteLinks(linkIds string) error {
	cmd := fmt.Sprintf(utils.CmdDeleteLinks, linkIds)
	_, err := utils.RunCommand(cmd)
	return err
}

func AddLink(hbInfo map[string]string, linkId string) error {
	hbInfoStr := GenerateLinkStr(hbInfo)
	var cmd string
	if linkId == "" {
		cmd = fmt.Sprintf(utils.CmdAddLinkForce, hbInfoStr)
	} else {
		cmd = fmt.Sprintf(utils.CmdAddLinksWithLinkNum, hbInfoStr, linkId)
	}

	_, err := utils.RunCommand(cmd)
	return err
}

func EditLinks(hbInfo map[string]string, linkId string) error {
	hbInfoStr := GenerateLinkStr(hbInfo)
	cmd := fmt.Sprintf(utils.CmdUpdateLinkForce, hbInfoStr, linkId)
	_, err := utils.RunCommand(cmd)
	return err
}

func GenerateLinkStr(data map[string]string) string {
	var linkStr strings.Builder
	for k, v := range data {
		linkStr.WriteString(" ")
		linkStr.WriteString(k)
		linkStr.WriteString("=")
		linkStr.WriteString(v)
	}
	return linkStr.String()[1:]
}

// HBInfo represents the heartbeat information structure
type HBInfo struct {
	Data        []map[string]string `json:"data"`
	ClusterName string              `json:"cluster_name"`
}

// ExtractHbInfo reorganizes heartbeat information format
func ExtractHbInfo(hbInfo []map[string]string) ([]map[string]string, []string) {
	var hbDictList []map[string]string

	if len(hbInfo) == 0 {
		return nil, nil
	}
	var numIds []string
	for key := range hbInfo[0] {
		if strings.HasPrefix(key, "ring") {
			numIds = append(numIds, key[4:5]) // ring0-ring7
		}
	}

	for _, id := range numIds {
		hbDict := map[string]string{}
		for _, nodeInfo := range hbInfo {
			nodeName := nodeInfo["name"]
			ringAddr := nodeInfo["ring"+id+"_addr"]
			if ringAddr != "" {
				hbDict[nodeName] = nodeInfo["ring"+id+"_addr"]
			}
		}
		hbDictList = append(hbDictList, hbDict)
	}
	return hbDictList, numIds
}

// get net heartbeat info from corosync conf
func ExtractHbInfoFromConf() (map[string][]string, []string) {
	linksInfo := make(map[string][]string)
	hosts := []string{}

	file, err := os.Open(settings.CorosyncConfFile)
	if err != nil {
		slog.Error(fmt.Sprintf("Error opening file: %v", err))
		return linksInfo, hosts
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, "ring") && !strings.Contains(line, "disk") {
			id := strings.TrimSpace(line)[4:5]
			parts := strings.Fields(line)
			if len(parts) > 1 {
				ip := parts[1]
				linksInfo[id] = append(linksInfo[id], ip)
			}
		}
		if strings.Contains(line, "name:") && !strings.Contains(line, "cluster_name") {
			parts := strings.Fields(line)
			if len(parts) > 1 {
				hosts = append(hosts, parts[1])
			}
		}
	}

	if err := scanner.Err(); err != nil {
		slog.Error(fmt.Sprintf("Error reading file: %v", err))
	}

	return linksInfo, hosts
}

func GetRingIdFromIPOnline(ipAddress string) (int, error) {
	// IPv4 check
	if !isValidIPv4(ipAddress) {
		return -1, fmt.Errorf("%s is not an IPv4 address", ipAddress)
	}

	output, err := utils.RunCommand(utils.CmdHbStatus)
	if err != nil {
		return -1, err
	}

	// Parse the output
	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, "LINK") {
			linkID := strings.Fields(line)[1]
			ipStr := strings.Split(strings.Split(line, "udp (")[1], ")")[0]
			ips := strings.Split(ipStr, "->")
			for _, ip := range ips {
				if strings.TrimSpace(ip) == ipAddress {
					id, err := strconv.Atoi(linkID)
					if err != nil {
						return -1, err
					}
					return id, nil
				}
			}
		}
	}

	return -1, fmt.Errorf("unable to find the link for the corresponding IP information")
}

// getRingIDFromIPOffline attempts to get the heartbeat ID from an IP address using an offline method.
func GetRingIdFromIPOffline(ipAddress string) (int, error) {
	file, err := os.Open(settings.CorosyncConfFile)
	if err != nil {
		return -1, err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, "ring") && strings.Contains(line, ipAddress) {
			idStr := strings.Split(strings.TrimSpace(line), "_")[0]
			id, err := strconv.Atoi(idStr[len(idStr)-1:])
			if err != nil {
				return -1, err
			}
			return id, nil
		}
	}

	if err := scanner.Err(); err != nil {
		return -1, err
	}

	return -1, fmt.Errorf("unable to find the link for the corresponding IP information")
}

// isValidIPv4 checks if the given string is a valid IPv4 address.
func isValidIPv4(ip string) bool {
	return net.ParseIP(ip) != nil && strings.Contains(ip, ".")
}

type AddHeartbeatRequest struct {
	ClusterName string         `json:"cluster_name"`
	Data        []AddHeartData `json:"data"`
}

type AddHeartData struct {
	Name     string   `json:"name"`
	RingData []string `json:"ring_data"` // IP 列表
}

// 承接编辑心跳、删除心跳请求数据
type HeartbeatRequest struct {
	ClusterName string      `json:"cluster_name"`
	Data        []HeartData `json:"data"`
}

type HeartData struct {
	Name     string       `json:"name"`
	RingData []RingDetail `json:"ring_data"` // 包含 ring_name 和 ip
}

type RingDetail struct {
	RingName string `json:"ring_name"`
	IP       string `json:"ip"`
}

func DeleteHeartbeat(hbInfo HeartbeatRequest) utils.GeneralResponse {
	hbData := hbInfo.Data
	if len(hbData) == 0 {
		return utils.GeneralResponse{
			Action: false,
			Error:  gettext.Gettext("input validation failed"),
		}
	}

	minLinkNum := 1
	currentLocalHbIds := GetCurrentLinkIds()
	ringData := hbData[0].RingData
	// 至少要保留一路心跳
	if len(currentLocalHbIds) <= minLinkNum || len(currentLocalHbIds) <= len(ringData) {
		return utils.GeneralResponse{
			Action: false,
			Error:  gettext.Gettext("At least one heartbeat needs to be preserved and cannot be deleted further."),
		}
	}
	var linkId int
	var linkIdSet []string

	status := GetClusterStatus()
	for _, ring := range ringData {
		ip := ring.IP
		if status == 0 {
			linkId, _ = GetRingIdFromIPOnline(ip)
			if linkId == -1 {
				linkId, _ = GetRingIdFromIPOffline(ip)
			}
		} else {
			linkId, _ = GetRingIdFromIPOffline(ip)
		}

		if linkId == -1 {
			return utils.GeneralResponse{
				Action: false,
				Error:  gettext.Gettext("Exception occurred while deleting the heartbeat. Please check the logs and the original heartbeat status."),
			}
		}

		linkIdSet = append(linkIdSet, strconv.Itoa(linkId))
	}

	linkIdSetStr := strings.Join(linkIdSet, " ")
	cmd := fmt.Sprintf(utils.CmdDeleteLinks, linkIdSetStr)
	if _, err := utils.RunCommand(cmd); err != nil {
		return utils.GeneralResponse{
			Action: false,
			Error:  gettext.Gettext("Deletion heartbeat failed, the heartbeat ID does not exist"),
		}
	}

	//更新并同步配置文件
	localCluster := LocalClusterInfo()
	if err := UpdateClusterConfFile(localCluster); err != nil {
		return utils.GeneralResponse{
			Action: false,
			Info:   gettext.Gettext("Failed to synchronize Cluster file"),
		}
	}

	return utils.GeneralResponse{
		Action: true,
		Info:   gettext.Gettext("Delete heartbeat success"),
	}
}

// 将添加心跳的信息重新组织
func generateHbInfoList(addHbData []AddHeartData) []map[string]string {
	var res []map[string]string
	for i, _ := range addHbData[0].RingData {
		ringInfo := map[string]string{}
		for j, hbData := range addHbData {
			nodeName := hbData.Name
			ringInfo[nodeName] = addHbData[j].RingData[i]
		}
		res = append(res, ringInfo)
	}
	return res
}

func AddHeartbeat(hbInfo AddHeartbeatRequest) utils.GeneralResponse {
	specialLinkId := "7"
	MaxLinkCount := 7
	hbData := hbInfo.Data
	if len(hbData) == 0 {
		return utils.GeneralResponse{
			Action: false,
			Error:  gettext.Gettext("input validation failed"),
		}
	}

	hbIds := GetCurrentLinkIds()
	if utils.Contains(hbIds, specialLinkId) {
		utils.RemoveByValue(hbIds, specialLinkId)
	}
	if len(hbIds) >= MaxLinkCount {
		return utils.GeneralResponse{
			Action: false,
			Error:  gettext.Gettext("The maximum number of heartbeats in the cluster has been reached and cannot be added further."),
		}
	}
	clusterStatus := GetClusterStatus()
	if clusterStatus == 0 {
		connectedNetLinksId := GetConnectedNetLinksId()
		if len(connectedNetLinksId) < 1 {
			return utils.GeneralResponse{
				Action: false,
				Error:  gettext.Gettext("All network heartbeats currently in a disconnected state and cannot continue to add heartbeat."),
			}
		}
	}
	hbInfoList := generateHbInfoList(hbInfo.Data)
	for _, hbInfo := range hbInfoList {
		if err := AddLink(hbInfo, ""); err != nil {
			return utils.GeneralResponse{
				Action: false,
				Error:  gettext.Gettext("Exception occurred while adding heartbeats, either the IP was conflicted with the original heartbeat"),
			}
		}
	}

	//更新并同步配置文件
	localCluster := LocalClusterInfo()
	if err := UpdateClusterConfFile(localCluster); err != nil {
		return utils.GeneralResponse{
			Action: false,
			Info:   gettext.Gettext("Failed to synchronize Cluster file"),
		}
	}
	return utils.GeneralResponse{
		Action: true,
		Info:   gettext.Gettext("Add heartbeat success"),
	}
}

func GetConnectedNetLinksId() []string {
	out, err := utils.RunCommand(utils.CmdHbStatusS)
	if err != nil {
		return nil
	}
	var ConnectedLinkIds []string

	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
	found := false
	for _, line := range lines {
		if strings.Contains(line, "LINK ID") {
			id := strings.Split(strings.TrimSpace(line), " ")[2]
			ConnectedLinkIds = append(ConnectedLinkIds, id)
			found = true
		}
		if found && (strings.Contains(line, "disk") || strings.Contains(line, "disconnected")) {
			_, ConnectedLinkIds = utils.Pop(ConnectedLinkIds)
			found = false
		}
	}
	return ConnectedLinkIds
}

func SyncCorosyncConf() error {
	_, err := utils.RunCommand(utils.CmdSyncCorosyncConf)
	return err
}

func EditHeartbeat(hbInfo HeartbeatRequest) utils.GeneralResponse {
	maxNum := 7
	hbData := hbInfo.Data
	var conf utils.CorosyncConfig
	if len(hbData) == 0 {
		return utils.GeneralResponse{
			Action: false,
			Error:  gettext.Gettext("input validation failed"),
		}
	}
	ringData := hbData[0].RingData

	// 目前真实的心跳标号情况
	hbIds := GetCurrentLinkIds()
	if len(hbIds) == 0 {
		return utils.GeneralResponse{
			Action: false,
			Error:  gettext.Gettext("The cluster has not been created."),
		}
	}

	// 要编辑的心跳的信息
	var idsEdited []string
	var hbInfoList []map[string]string
	for i, ring := range ringData { // 心跳
		ids := ring.RingName[4:5]
		idsEdited = append(idsEdited, ids)
		ringInfo := map[string]string{}
		for j, _ := range hbData { // 节点
			nodeName := hbData[j].Name
			ringInfo[nodeName] = hbData[j].RingData[i].IP
		}
		hbInfoList = append(hbInfoList, ringInfo)
	}

	idsNum := len(hbInfoList)
	if idsNum > maxNum {
		return utils.GeneralResponse{
			Action: false,
			Error:  gettext.Gettext("The heartbeat to be edited exceeds the maximum number of heartbeats"),
		}
	}
	clusterStatus := GetClusterStatus()
	// 启动走 pcs cluster link 方案
	if clusterStatus == 0 {
		connectedNetIds := GetConnectedNetLinksId()
		leftConnectedIds := utils.DifferenceSlice(connectedNetIds, idsEdited)
		if len(hbInfoList) < len(hbIds) && len(leftConnectedIds) > 0 {
			var linksStr strings.Builder
			for _, id := range idsEdited {
				linksStr.WriteString(" ")
				linksStr.WriteString(id)
			}
			// 基于心跳编号删除要编辑的心跳
			DeleteLinks(linksStr.String())

			// 添加新的心跳内容
			for _, x := range hbInfoList {
				AddLink(x, "")
			}
			goto ret
		}
	}

	// 集群未启动走修改配置文件的方案
	utils.RunCommand(utils.CmdStopCluster + " --force")

	// 修改配置文件
	conf, _ = utils.GetCorosyncConfig()
	for _, nodeInfo := range hbData {
		for _, node := range conf.NodeList {
			if nodeInfo.Name == node["name"] {
				for _, ring := range nodeInfo.RingData {
					node[ring.RingName] = ring.IP
				}
			}
		}
	}

	utils.SetCorosyncConfig(conf, settings.CorosyncConfFile)
	if err := SyncCorosyncConf(); err != nil {
		return utils.GeneralResponse{
			Action: false,
			Error:  gettext.Gettext("Failed to synchronize corosync.conf file"),
		}
	}
	// 重新启动集群
	utils.RunCommand(utils.CmdStartCluster)

ret:
	//更新并同步配置文件
	localCluster := LocalClusterInfo()
	if err := UpdateClusterConfFile(localCluster); err != nil {
		return utils.GeneralResponse{
			Action: false,
			Info:   gettext.Gettext("Failed to synchronize Cluster file"),
		}
	}

	return utils.GeneralResponse{
		Action: true,
		Info:   gettext.Gettext("Edit heartbeat success"),
	}
}

func GetCurrentLinkIds() []string {
	linksInfo, _ := ExtractHbInfoFromConf()
	ids := make([]string, 0, len(linksInfo))
	for k := range linksInfo {
		ids = append(ids, k)
	}
	return ids
}
